"""Listen to Kubernetes events and print them to stdout."""
import argparse
import functools
from http import HTTPStatus
import pathlib

from cki_lib import logger
from cki_lib import metrics
from cki_lib import misc
from kubernetes import client
from kubernetes import config
from kubernetes import watch
import prometheus_client
import sentry_sdk
import urllib3.exceptions

LOGGER = logger.get_logger(__name__)

METRIC_EVENTS_COUNT = prometheus_client.Counter(
    'cki_k8s_event_received',
    'Number of k8s events received',
    ['type']
)

METRIC_EVENTS_OOM_COUNT = prometheus_client.Counter(
    'cki_k8s_oom_event_generated',
    'Number of k8s oom events generated')


class EventListener():
    """Listen for events and log/evaluate them."""

    def __init__(self, retry=True):
        """Run events listener."""
        self.retry = retry

        config.load_incluster_config()
        self.v1 = client.CoreV1Api()

        # Initialize labels otherwise they're only published
        # once the first message arrives.
        METRIC_EVENTS_COUNT.labels('Warning')
        METRIC_EVENTS_COUNT.labels('Normal')

    @functools.cached_property
    def namespace(self):
        """Allow to override the namespace in unit tests."""
        return pathlib.Path(
            '/var/run/secrets/kubernetes.io/serviceaccount/namespace'
        ).read_text(encoding='utf8')

    def process_pod_oom(self, event):
        """Create an event if a pod OOM is detected."""
        if event.reason != 'Started' or event.involved_object.kind != 'Pod':
            LOGGER.debug("Not a pod starting, not checking for OOM.")
            return
        try:
            pod = self.v1.read_namespaced_pod(event.involved_object.name,
                                              event.involved_object.namespace)
        except client.rest.ApiException as exc:
            if exc.status == HTTPStatus.NOT_FOUND:
                return
            raise

        for status in pod.status.container_statuses:
            if (not status.last_state.terminated or
                    status.last_state.terminated.reason != 'OOMKilled'):
                LOGGER.debug("Not an OOM-killed pod.")
                return

            self.v1.create_namespaced_event(
                event.involved_object.namespace,
                client.CoreV1Event(
                    metadata=client.V1ObjectMeta(generate_name=event.involved_object.name),
                    involved_object=event.involved_object,
                    type='Warning',
                    reason='PreviousContainerWasOOMKilled',
                    message=f'The previous instance of the container "{status.name}" was OOMKilled',
                ))
            METRIC_EVENTS_OOM_COUNT.inc()

    @staticmethod
    def log_event(event):
        """Log an event."""
        event_timestamp = event.last_timestamp or event.metadata.creation_timestamp
        print(
            # Remove tz info to match promtail timestamp parser
            event_timestamp.replace(tzinfo=None).isoformat() +
            f' - [{event.type}] - {event.involved_object.kind} - '
            f'{event.involved_object.name} - {event.message} - ({event.reason})'
        )

    def run(self):
        """Run events listener."""
        while True:
            start_timestamp = misc.now_tz_utc()
            try:
                for raw_event in watch.Watch().stream(
                    self.v1.list_namespaced_event,
                    namespace=self.namespace,
                ):
                    event = raw_event['object']

                    event_timestamp = event.last_timestamp or event.metadata.creation_timestamp
                    if event_timestamp < start_timestamp:
                        # At the beginning, list_namespaced_event returns a list of
                        # old events. We only care for the ones that happen from start_timestamp
                        # to avoid sending duplicated lines.
                        LOGGER.debug("Old event, not logging.")
                        continue

                    METRIC_EVENTS_COUNT.labels(event.type).inc()
                    self.log_event(event)
                    self.process_pod_oom(event)
            except client.rest.ApiException as exc:
                if exc.status != HTTPStatus.GONE:
                    raise
            except urllib3.exceptions.ProtocolError:
                # "Response ended prematurely" if there are no events at all
                pass

            if not self.retry:
                break


def run(args: list[str] | None = None) -> None:
    """Run the selected listener."""
    parser = argparse.ArgumentParser()
    parser.parse_args(args)

    metrics.prometheus_init()

    EventListener().run()


if __name__ == '__main__':
    misc.sentry_init(sentry_sdk)
    run()
